/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {
  ArkNormalBinopExpr,
  ArkConditionExpr,
  ArkAssignStmt,
  Constant,
  Local,
  NormalBinaryOperator,
  Stmt,
  Value,
  RelationalBinaryOperator,
  ArkIfStmt,
  BasicBlock,
  Type,
  BinaryOperator,
} from 'arkanalyzer/lib';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import {
  MatcherCallback,
  MatcherTypes,
  StmtMatcher,
} from '../../matcher/Matchers';
import { Defects, IssueReport } from '../../model/Defects';
import { Rule } from '../../model/Rule';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { BaseChecker, BaseMetaData } from '../BaseChecker';

const logger = Logger.getLogger(
  LOG_MODULE_TYPE.HOMECHECK,
  'PreferNullishCoalescing'
);

interface Condition {
  bigint: false;
  boolean: false;
  number: false;
  string: false;
}

type NullCheckInfo = {
  checkedVar: Local;
  isNegated: boolean;
};

interface Options {
  allowRuleToRunWithoutStrictNullChecks?: boolean;
  ignoreConditionalTests?: boolean;
  ignoreTernaryTests?: boolean;
  ignoreMixedLogicalExpressions?: boolean;
  ignorePrimitives?: Condition;
}

export class PreferNullishCoalescingCheck implements BaseChecker {
  private processedStatements = new Set<string>();
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  private nullCheckVariables = new Map<string, NullCheckInfo>();
  private defaultOptions: Options = {
    ignoreConditionalTests: false,
    ignoreTernaryTests: false,
    ignoreMixedLogicalExpressions: false,
    ignorePrimitives: {
      bigint: false,
      boolean: false,
      number: false,
      string: false,
    },
  };
  public metaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/prefer-nullish-coalescing.md',
    description:
      'Enforce using nullish coalescing operator instead of logical OR',
  };

  private options: Options;

  private stmtMatcher: StmtMatcher = {
    matcherType: MatcherTypes.STMT,
    match(stmt: Stmt): boolean {
      return stmt instanceof ArkAssignStmt || stmt instanceof ArkIfStmt;
    },
  };

  public registerMatchers(): MatcherCallback[] {
    const matchfileBuildCb: MatcherCallback = {
      matcher: this.stmtMatcher,
      callback: this.check,
    };
    return [matchfileBuildCb];
  }

  public check = (stmt: ArkAssignStmt | ArkIfStmt): void => {
    this.options =
      this.rule && this.rule.option && this.rule.option[0]
        ? (this.rule.option[0] as Options)
        : this.defaultOptions;
    if (stmt instanceof ArkIfStmt) {
      this.checkIfStmtForTernary(stmt);
      return;
    }

    // 处理赋值语句
    this.checkAssignStmt(stmt);
  };

  /**
   * 检查赋值语句
   */
  private checkAssignStmt(stmt: ArkAssignStmt): void {
    const rightOp = stmt.getRightOp();
    const leftOp = stmt.getLeftOp();

    // 提取条件表达式和局部变量
    const { expr, localExpr } = this.extractExprAndLocal(rightOp, leftOp);
    if (!expr || !localExpr) {
      return;
    }

    // 根据条件表达式的类型进行处理
    if (expr instanceof ArkConditionExpr) {
      this.checkConditionalExpr(expr, localExpr);
    } else if (expr instanceof ArkNormalBinopExpr) {
      this.checkNormalBinopExpr(stmt, expr, localExpr);
    }
  }

  /**
   * 提取条件表达式和局部变量
   */
  private extractExprAndLocal(
    rightOp: Value,
    leftOp: Value
  ): {
    expr: ArkConditionExpr | ArkNormalBinopExpr | null;
    localExpr: Local | null;
  } {
    if (
      (rightOp instanceof ArkConditionExpr ||
        rightOp instanceof ArkNormalBinopExpr) &&
      leftOp instanceof Local
    ) {
      return { expr: rightOp, localExpr: leftOp };
    }

    if (
      (leftOp instanceof ArkConditionExpr ||
        leftOp instanceof ArkNormalBinopExpr) &&
      rightOp instanceof Local
    ) {
      return { expr: leftOp, localExpr: rightOp };
    }

    return { expr: null, localExpr: null };
  }

  private checkIfStmtForTernary(stmt: ArkIfStmt): void {
    const IF_KEYWORD = 'if';
    if (this.options.ignoreTernaryTests) {
      return;
    }
    const originalText = stmt.getOriginalText();
    const condition = stmt.getConditionExpr();
    if (originalText?.includes(IF_KEYWORD) || !condition) {
      return;
    }
    let checkInfo = this.getNullCheckInfo(condition);
    if (!checkInfo) {
      const op1 = condition.getOp1();
      if (op1 instanceof Local) {
        checkInfo = this.nullCheckVariables.get(op1.getName());
      }
    }
    if (!checkInfo) {
      return;
    }
    for (const block of stmt.getCfg().getBlocks()) {
      if (this.checkBlockForTernary(stmt, block, checkInfo)) {
        break;
      }
    }
  }

  /**
   * 检查控制流块是否符合三目运算符的优化条件
   */
  private checkBlockForTernary(
    stmt: ArkIfStmt,
    block: BasicBlock,
    checkInfo: NullCheckInfo
  ): boolean {
    for (const blockStmt of block.getStmts()) {
      // 如果块中的语句不是 ArkIfStmt 或与原始语句不匹配，直接返回
      if (
        !(blockStmt instanceof ArkIfStmt) ||
        blockStmt.getOriginalText() !== stmt.getOriginalText()
      ) {
        continue;
      }
      // 根据条件信息获取后继语句
      const successorStmt = this.getSuccessorStmt(block, checkInfo);
      if (
        successorStmt instanceof ArkAssignStmt &&
        successorStmt.getRightOp() instanceof Local
      ) {
        const rightOp = successorStmt.getRightOp() as Local;

        // 如果右操作数与条件中检查的变量匹配，则报告问题
        if (rightOp.getName() === checkInfo.checkedVar.getName()) {
          const message =
            'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.';
          this.addIssueReport(stmt, message);
          return true;
        }
      }
    }
    return false;
  }

  /**
   * 获取后继语句，根据条件信息判断是否取反
   */
  private getSuccessorStmt(
    block: BasicBlock,
    checkInfo: NullCheckInfo
  ): Stmt | undefined {
    const successors = block.getSuccessors();
    if (!successors || successors.length < 2) {
      return undefined;
    }

    // 根据是否取反选择正确的后继语句
    return checkInfo.isNegated
      ? successors[0]?.getStmts()[0]
      : successors[1]?.getStmts()[0];
  }

  private checkConditionalExpr(
    conditionExpr: ArkConditionExpr,
    localExpr: Local
  ): void {
    if (this.options.ignoreTernaryTests) {
      return;
    }
    const checkInfo = this.getNullCheckInfo(conditionExpr);
    if (checkInfo) {
      this.nullCheckVariables.set(localExpr.getName(), checkInfo);
    }
  }

  private checkNormalBinopExpr(
    stmt: ArkAssignStmt,
    binopExpr: ArkNormalBinopExpr,
    localExpr: Local
  ): void {
    if (
      this.options.ignoreMixedLogicalExpressions &&
      this.isMixedLogicalExpression(stmt)
    ) {
      return;
    }
    const operator = binopExpr.getOperator();
    if (
      operator === NormalBinaryOperator.LogicalOr ||
      operator === NormalBinaryOperator.LogicalAnd
    ) {
      this.checkLogicalCombination(stmt, binopExpr, localExpr);
    }
    // c = a || b 的情况
    if (operator === NormalBinaryOperator.LogicalOr) {
      this.checkLogicalOr(stmt, binopExpr);
    }
  }

  private isMixedLogicalExpression(stmt: ArkAssignStmt): boolean {
    const operators = new Set<BinaryOperator>();
    const getOperator = (stmt: ArkAssignStmt): void => {
      const rightOp = stmt.getRightOp();
      if (rightOp instanceof ArkNormalBinopExpr) {
        operators.add(rightOp.getOperator());
        const op1 = rightOp.getOp1();
        const op2 = rightOp.getOp2();
        if (
          op1 instanceof Local &&
          op1.getDeclaringStmt() instanceof ArkAssignStmt
        ) {
          getOperator(op1.getDeclaringStmt() as ArkAssignStmt);
        }
        if (
          op2 instanceof Local &&
          op2.getDeclaringStmt() instanceof ArkAssignStmt
        ) {
          getOperator(op2.getDeclaringStmt() as ArkAssignStmt);
        }
      }
    };
    getOperator(stmt);
    return (
      operators.has(NormalBinaryOperator.LogicalAnd) &&
      operators.has(NormalBinaryOperator.LogicalOr)
    );
  }

  private checkLogicalCombination(
    stmt: ArkAssignStmt,
    binopExpr: ArkNormalBinopExpr,
    localExpr: Local
  ): void {
    if (this.options.ignoreTernaryTests) {
      return;
    }
    const op1 = binopExpr.getOp1();
    const op2 = binopExpr.getOp2();
    if (!(op1 instanceof Local && op2 instanceof Local)) {
      return;
    }
    const info1 = this.nullCheckVariables.get(op1.getName());
    const info2 = this.nullCheckVariables.get(op2.getName());
    if (
      info1 &&
      info2 &&
      info1.checkedVar.getName() === info2.checkedVar.getName() &&
      info1.isNegated === info2.isNegated
    ) {
      this.nullCheckVariables.set(localExpr.getName(), info1);
    }
  }

  private checkLogicalOr(
    stmt: ArkAssignStmt,
    binopExpr: ArkNormalBinopExpr
  ): void {
    if (this.options.ignoreConditionalTests) {
      return;
    }
    const op1 = binopExpr.getOp1();
    if (op1 instanceof Local) {
      if (!this.isTypeNullable(op1.getType())) {
        return;
      }
      if (this.isIgnoredPrimitive(op1.getType().getTypeString() ?? '')) {
        return;
      }
      const message =
        'Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.';
      this.addIssueReport(stmt, message);
    }
  }

  private isTypeNullable(declaration: Type): boolean {
    if (!declaration || !(declaration instanceof Type)) {
      return false;
    }
    const typeName = declaration.getTypeString() ?? '';
    return typeName
      .split('|')
      .some((part) => part.trim() === 'null' || part.trim() === 'undefined');
  }

  private isIgnoredPrimitive(type: string): boolean {
    if (!this.options.ignorePrimitives || !type) {
      return false;
    }

    const typeName = type.replace(/\|null|\|undefined/g, '').trim();
    const primitives = this.options.ignorePrimitives;

    return (
      (primitives.string && typeName === 'string') ||
      (primitives.number && typeName === 'number') ||
      (primitives.boolean && typeName === 'boolean') ||
      (primitives.bigint && typeName === 'bigint')
    );
  }

  private getNullCheckInfo(expr: ArkConditionExpr): NullCheckInfo | undefined {
    const op = expr.getOperator();
    const op1 = expr.getOp1();
    const op2 = expr.getOp2();
    const isNegated =
      op === RelationalBinaryOperator.StrictInequality ||
      op === RelationalBinaryOperator.InEquality;
    if (this.isValueNull(op2) || this.isValueUndefined(op2)) {
      return { checkedVar: op1 as Local, isNegated };
    } else if (this.isValueNull(op1) || this.isValueUndefined(op1)) {
      return { checkedVar: op2 as Local, isNegated };
    }
    return undefined;
  }

  private isValueNull(val: Value): boolean {
    return val instanceof Constant && val.getValue() === 'null';
  }

  private isValueUndefined(val: Value): boolean {
    return val instanceof Constant && val.getValue() === 'undefined';
  }

  private addIssueReport(stmt: Stmt, message: string): void {
    const position = stmt.getOriginPositionInfo();
    const stmtHash = `${stmt.getOriginalText()}_${position.getLineNo()}_${position.getColNo()}`;
    if (this.processedStatements.has(stmtHash)) {
      return;
    }
    this.processedStatements.add(stmtHash);
    const severity = this.rule.alert ?? this.metaData.severity;
    const lineAndCol = this.getLineAndColumns(stmt);

    if (lineAndCol.line !== -1) {
      const defect = new Defects(
        lineAndCol.line,
        lineAndCol.startCol,
        lineAndCol.endCol,
        message,
        severity,
        this.rule.ruleId,
        lineAndCol.filePath,
        this.metaData.ruleDocPath,
        true,
        false,
        false
      );
      this.issues.push(new IssueReport(defect, undefined));
      RuleListUtil.push(defect);
    }
  }

  private getLineAndColumns(stmt: Stmt): {
    line: number;
    startCol: number;
    endCol: number;
    filePath: string;
  } {
    const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
    const filePath = arkFile?.getFilePath() ?? '';

    const posInfo = stmt.getOriginPositionInfo();
    const opPosInfo = stmt.getOperandOriginalPositions();
    if (!posInfo) {
      return { line: -1, startCol: -1, endCol: -1, filePath: filePath };
    }

    const line = posInfo.getLineNo();
    const startCol = posInfo.getColNo();
    const endCol = startCol + (stmt.getOriginalText()?.length ?? 0);

    if (opPosInfo && opPosInfo.length > 0) {
      const opPos = opPosInfo[0];
      const opStartCol = opPos.getFirstCol();
      return { line, startCol: opStartCol, endCol, filePath };
    }
    return { line, startCol, endCol, filePath };
  }
}
